package core;

import java.util.ArrayList;

import core.command.AddStrokeCommand;
import core.command.DeleteStrokesCommand;
import core.command.HorizontalFlipCommand;
import core.command.SelectionCommand;
import core.command.Memento;
import core.command.TransformStrokesCommand;
import core.command.TranslateStrokesCommand;
import core.command.VerticalFlipCommand;

public class UserContext {
	private Palette palette = new Palette();
	private CursorContainer cursorContainer = new CursorContainer();
	private Memento memento = new Memento();
	private Drawing drawing = null;
	
	private boolean isTranslating = false;
	private boolean isTransforming = false;

	private ArrayList< Stroke > selectedStrokes = new ArrayList< Stroke >();

	public UserContext( Drawing d ) {
		drawing = d;
	}
	
	public void setPositionOfCenterOfPalette( float x, float y ) {
		palette.x0 = Math.round( x - palette.width/2 );
		palette.y0 = Math.round( y - palette.height/2 );
	}
	public void movePalette(
		float delta_x, float delta_y // displacement, in pixels
	) {
		palette.x0 += Math.round( delta_x );
		palette.y0 += Math.round( delta_y );
	}
	
	public Palette getPalette()
	{
		return palette;
	}

	// returns true if any cursors are currently being handled by this user context
	public boolean hasCursors() {
		return cursorContainer.getNumCursors() > 0;
	}

	public int getNumCursors() {
		return cursorContainer.getNumCursors();
	}

	// returns true if the given cursor (identified by its id)
	// is currently being handled by this user context
	public boolean hasCursorID( int id ) {
		return cursorContainer.findIndexOfCursorById(id) > -1;
	}

	// returns the distance between the given point and the center of the palette of this user context
	public float distanceToPalette(
		float x, float y // pixel coordinates
	) {
		return Point2D.diff( palette.getCenter(), new Point2D(x,y) ).length();
	}


	public void draw( GraphicsWrapper gw ) {

		palette.draw( gw );

		// draw filled rectangles over the selected strokes
		gw.setCoordinateSystemToWorldSpaceUnits();
		for ( Stroke s : selectedStrokes ) {
			AlignedRectangle2D r = s.getBoundingRectangle();
			gw.setColor(1.0f,0.5f,0,0.2f); // transparent orange
			Vector2D diagonal = r.getDiagonal();
			gw.fillRect( r.getMin().x(), r.getMin().y(), diagonal.x(), diagonal.y() );
		}

		gw.setCoordinateSystemToPixels();

		// draw cursors
		for ( int i = 0; i < cursorContainer.getNumCursors(); ++i ) {
			MyCursor cursor = cursorContainer.getCursorByIndex( i );
			if ( cursor.type == MyCursor.TYPE_NOTHING )
				gw.setColor(0.5f,0,0,0.65f); // red (because this cursor is being ignored)
			else
				gw.setColor(0,0.5f,0.5f,0.65f); // cyan
			gw.fillCircle( cursor.getCurrentPosition().x()-10, cursor.getCurrentPosition().y()-10, 10 );

			if ( cursor.type == MyCursor.TYPE_INKING ) {
				// draw ink trail
				gw.setColor(0,0,0);
				gw.drawPolyline( cursor.getPositions() );
			}
			else if ( cursor.type == MyCursor.TYPE_SELECTION ) {
				if ( cursor.doesDragLookLikeLassoGesture() ) {
					// draw filled polygon
					gw.setColor(0,0,0,0.2f);
					gw.fillPolygon( cursor.getPositions() );
				}
				else {
					// draw polyline to indicate that a lasso could be started
					gw.setColor(0,0,0);
					gw.drawPolyline( cursor.getPositions() );

					// also draw selection rectangle
					gw.setColor(0,0,0,0.2f);
					Vector2D diagonal = Point2D.diff( cursor.getCurrentPosition(), cursor.getFirstPosition() );
					gw.fillRect( cursor.getFirstPosition().x(), cursor.getFirstPosition().y(), diagonal.x(), diagonal.y() );
				}
			}
		}
	}




	// returns true if a redraw is requested
	public boolean processMultitouchInputEvent(
		int id,
		float x, // in pixels
		float y, // in pixels
		int type,
		GraphicsWrapper gw,
		boolean doOtherUserContextsHaveCursors
	) {
		// Find the cursor that corresponds to the event id, if such a cursor already exists.
		// If no such cursor exists, the below index will be -1, and the reference to cursor will be null.
		int cursorIndex = cursorContainer.findIndexOfCursorById( id );
		MyCursor cursor = (cursorIndex==-1) ? null : cursorContainer.getCursorByIndex( cursorIndex );


		if ( cursor == null ) {

			if ( type == MultitouchFramework.TOUCH_EVENT_UP ) {
				// This should never happen, but if it does, just ignore the event.
				return false;
			}

			// The event does not correspond to any existing cursor.
			// In other words, this is a new finger touching the screen.
			// The event is probably of type TOUCH_EVENT_DOWN.
			// A new cursor will need to be created for the event.

			if ( palette.contains( x, y ) ) {
				// The event occurred inside the palette.

				if ( cursorContainer.getNumCursors() == 0 ) {
					// There are currently no cursors engaged for this user context.
					// In other words, this new finger is the only finger for the user context.
					// So, we allow the event for the new finger to activate a button in the palette.
					// We branch according to the button under the event.
					//
					int indexOfButton = palette.indexOfButtonContainingTheGivenPoint( x, y );
					if (
						indexOfButton == palette.movePalette_buttonIndex
					) {
						palette.buttons.get( indexOfButton ).isPressed = true;

						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );
					}
					else if (
						indexOfButton == palette.ink_buttonIndex
						|| indexOfButton == palette.select_buttonIndex
						|| indexOfButton == palette.manipulate_buttonIndex
						|| indexOfButton == palette.camera_buttonIndex
					) {
						// We transition to the mode corresponding to the button
						palette.buttons.get( palette.currentlyActiveModalButton ).isPressed = false;
						palette.currentlyActiveModalButton = indexOfButton;
						palette.buttons.get( indexOfButton ).isPressed = true;

						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );
					}
					else if (
						indexOfButton == palette.black_buttonIndex
						|| indexOfButton == palette.red_buttonIndex
						|| indexOfButton == palette.green_buttonIndex
					) {
						// We transition to the color corresponding to the button
						palette.buttons.get( palette.currentlyActiveColorButton ).isPressed = false;
						palette.currentlyActiveColorButton = indexOfButton;
						palette.buttons.get( indexOfButton ).isPressed = true;

						if ( indexOfButton == palette.black_buttonIndex ) {
							palette.current_red = 0;
							palette.current_green = 0;
							palette.current_blue = 0;
						}
						else if ( indexOfButton == palette.red_buttonIndex ) {
							palette.current_red = 1.0f;
							palette.current_green = 0;
							palette.current_blue = 0;
						}
						else if ( indexOfButton == palette.green_buttonIndex ) {
							palette.current_red = 0;
							palette.current_green = 1.0f;
							palette.current_blue = 0;
						}

						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );
					}
					else if ( indexOfButton == palette.horizFlip_buttonIndex ) {
						palette.buttons.get( indexOfButton ).isPressed = true;

						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );

						// Flip the selected strokes horizontally (around a vertical axis)
						this.memento.doCommand(new HorizontalFlipCommand(this.drawing, this.selectedStrokes), false);
						
					}
					else if ( indexOfButton == palette.verticalFlip_buttonIndex ) {
						palette.buttons.get( indexOfButton ).isPressed = true;

						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );

						// Flip the selected strokes vertically (around a horizontal axis)
						this.memento.doCommand(new VerticalFlipCommand(this.drawing, this.selectedStrokes), false);
					}
					else if ( indexOfButton == palette.frameAll_buttonIndex ) {
						palette.buttons.get( indexOfButton ).isPressed = true;

						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );

						// Frame the entire drawing
						gw.frame( drawing.getBoundingRectangle(), true );
					}
					else if ( indexOfButton == palette.undo_buttonIndex ) {
						palette.buttons.get( indexOfButton ).isPressed = true;
						
						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );
						
						this.memento.undo();
					}
					else if ( indexOfButton == palette.redo_buttonIndex ) {
						palette.buttons.get( indexOfButton ).isPressed = true;
						
						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );
						
						this.memento.redo();
					}
					else if (indexOfButton == palette.deleteSelection_buttonIndex){
						palette.buttons.get( indexOfButton ).isPressed = true;
						
						// Cause a new cursor to be created to keep track of this event id in the future
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_INTERACTING_WITH_WIDGET, indexOfButton );
						
						//Delete the selection
						this.memento.doCommand(new DeleteStrokesCommand(this.drawing, this), false);
					}
					else {
						// The event occurred on some part of the palette where there are no buttons.
						// We cause a new cursor to be created to keep track of this event id in the future.
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );

						// Prevent the cursor from doing anything in the future.
						cursor.setType( MyCursor.TYPE_NOTHING );
					}
				}
				else {
					// There is already at least one cursor.
					// In other words, there is already one or more other fingers being tracked in this user context
					// (possibly on a palette button, and/or over the drawing).
					// To keep things simple, we prevent this new finger from doing anything.

					// We create a new cursor ...
					cursorIndex = cursorContainer.updateCursorById( id, x, y );
					cursor = cursorContainer.getCursorByIndex( cursorIndex );

					// ... and prevent the cursor from doing anything in the future.
					cursor.setType( MyCursor.TYPE_NOTHING );
				}
			}
			else {
				// The event did not occur inside the palette.
				// This new finger may have been placed down to start
				// drawing a stroke, or start camera manipulation, etc.
				// We branch according to the current mode.
				//
				if ( palette.currentlyActiveModalButton == palette.ink_buttonIndex ) {
					// start drawing a stroke
					cursorIndex = cursorContainer.updateCursorById( id, x, y );
					cursor = cursorContainer.getCursorByIndex( cursorIndex );
					cursor.setType( MyCursor.TYPE_INKING );
				}
				else if ( palette.currentlyActiveModalButton == palette.select_buttonIndex ) {
					// The new finger should only start selecting
					// if there is not already another finger performing selection.
					if ( cursorContainer.getNumCursorsOfGivenType( MyCursor.TYPE_SELECTION ) == 0 ) {
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_SELECTION );
					}
					else {
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_NOTHING );
					}
				}
				else if ( palette.currentlyActiveModalButton == palette.manipulate_buttonIndex ) {
					// The new finger should only manipulate the selection
					// if there are not already 2 fingers manipulating the selection.
					if ( cursorContainer.getNumCursorsOfGivenType( MyCursor.TYPE_DIRECT_MANIPULATION ) < 2 ) {
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_DIRECT_MANIPULATION );
					}
					else {
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_NOTHING );
					}
				}
				else if ( palette.currentlyActiveModalButton == palette.camera_buttonIndex ) {
					// The new finger should only manipulate the camera
					// if there are not already 2 fingers manipulating the camera.
					if ( cursorContainer.getNumCursorsOfGivenType( MyCursor.TYPE_CAMERA_PAN_ZOOM ) < 2 ) {
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_CAMERA_PAN_ZOOM );
					}
					else {
						cursorIndex = cursorContainer.updateCursorById( id, x, y );
						cursor = cursorContainer.getCursorByIndex( cursorIndex );
						cursor.setType( MyCursor.TYPE_NOTHING );
					}
				}

			}
		}
		else {
			// The event corresponds to an already existing cursor
			// (and the cursor was probably created during an earlier event of type TOUCH_EVENT_DOWN).
			// The current event is probably of type TOUCH_EVENT_MOVE or TOUCH_EVENT_UP.


			if ( type == MultitouchFramework.TOUCH_EVENT_MOVE ) {
				// The event is a move event, and corresponds to an existing cursor.
				// Is the location of the event different from the last reported location?
				Point2D newPosition = new Point2D( x, y );
				if ( cursor.getCurrentPosition().equals( newPosition ) ) {
					// The event's location is the same as last time.
					// Don't bother processing the event any further.
					return false; // do not request a redraw
				}
			}

			// We branch according to the type of cursor.
			//
			if ( cursor.type == MyCursor.TYPE_NOTHING ) {
				// Update the cursor with its new position.
				cursorContainer.updateCursorById( id, x, y );

				if ( type == MultitouchFramework.TOUCH_EVENT_UP )
					cursorContainer.removeCursorByIndex( cursorIndex );
			}
			else if ( cursor.type == MyCursor.TYPE_INTERACTING_WITH_WIDGET ) {
				if ( type == MultitouchFramework.TOUCH_EVENT_UP ) {
					// The user lifted their finger off of a palette button.
					cursorContainer.removeCursorByIndex( cursorIndex );

					if ( ! palette.buttons.get( cursor.indexOfButton ).isSticky ) {
						palette.buttons.get( cursor.indexOfButton ).isPressed = false;
					}
				}
				else {
					// Earlier, the user pressed down on a button in the palette,
					// and now they are dragging their finger over the button
					// (and possibly onto other buttons).
					// If this is the "move palette" button, we move the palette.
					if ( cursor.indexOfButton == palette.movePalette_buttonIndex ) {
						movePalette( x - cursor.getCurrentPosition().x(), y - cursor.getCurrentPosition().y() );
					}
					cursorIndex = cursorContainer.updateCursorById( id, x, y );
				}
			}
			else if ( cursor.type == MyCursor.TYPE_INKING ) {
				if ( type == MultitouchFramework.TOUCH_EVENT_UP ) {
					// up event
					cursorIndex = cursorContainer.updateCursorById( id, x, y );

					// Add the newly drawn stroke to the drawing
					Stroke newStroke = new Stroke();
					newStroke.setColor( palette.current_red, palette.current_green, palette.current_blue );
					for ( Point2D p : cursor.getPositions() ) {
						newStroke.addPoint( gw.convertPixelsToWorldSpaceUnits( p ) );
					}
					
					// Add the command to the stack of command and execute it
					this.memento.doCommand(new AddStrokeCommand(this.drawing, newStroke), false);

					cursorContainer.removeCursorByIndex( cursorIndex );
				}
				else {
					// drag event; just update the cursor with the new position
					cursorIndex = cursorContainer.updateCursorById( id, x, y );
				}
			}
			else if ( cursor.type == MyCursor.TYPE_CAMERA_PAN_ZOOM ) {
				if ( type == MultitouchFramework.TOUCH_EVENT_UP ) {
					// up event
					cursorContainer.removeCursorByIndex( cursorIndex );
				}
				else {
					// drag event
					cursorIndex = cursorContainer.updateCursorById( id, x, y );

					if ( cursorContainer.getNumCursorsOfGivenType( MyCursor.TYPE_CAMERA_PAN_ZOOM ) == 2 ) {
						MyCursor cursor0 = cursorContainer.getCursorByType( MyCursor.TYPE_CAMERA_PAN_ZOOM, 0 );
						MyCursor cursor1 = cursorContainer.getCursorByType( MyCursor.TYPE_CAMERA_PAN_ZOOM, 1 );
						gw.panAndZoomBasedOnDisplacementOfTwoPoints(
							id==cursor0.id ? cursor0.getPreviousPosition() : cursor0.getCurrentPosition(),
							id==cursor1.id ? cursor1.getPreviousPosition() : cursor1.getCurrentPosition(),
							cursor0.getCurrentPosition(),
							cursor1.getCurrentPosition()
						);
					}
					else if ( cursorContainer.getNumCursorsOfGivenType( MyCursor.TYPE_CAMERA_PAN_ZOOM ) == 1 ) {
						gw.pan(
							cursor.getCurrentPosition().x() - cursor.getPreviousPosition().x(),
							cursor.getCurrentPosition().y() - cursor.getPreviousPosition().y()
						);
					}
				}
			}
			else if ( cursor.type == MyCursor.TYPE_SELECTION ) {
				if ( type == MultitouchFramework.TOUCH_EVENT_UP ) {
					// up event
					cursorIndex = cursorContainer.updateCursorById( id, x, y );

					// Update the selection
					if ( cursor.doesDragLookLikeLassoGesture() ) {
						// complete a lasso selection

						// Need to transform the positions of the cursor from pixels to world space coordinates.
						// We will store the world space coordinates in the following data structure.
						ArrayList< Point2D > lassoPolygonPoints = new ArrayList< Point2D >();
						for ( Point2D p : cursor.getPositions() ) {
							lassoPolygonPoints.add( gw.convertPixelsToWorldSpaceUnits( p ) );
						}

						memento.doCommand(new SelectionCommand(drawing, lassoPolygonPoints, this), false);
					}
					else {
						// complete a rectangle selection

						AlignedRectangle2D selectedRectangle = new AlignedRectangle2D(
							gw.convertPixelsToWorldSpaceUnits( cursor.getFirstPosition() ),
							gw.convertPixelsToWorldSpaceUnits( cursor.getCurrentPosition() )
						);

						memento.doCommand(new SelectionCommand(drawing, selectedRectangle, this), false);
					}

					cursorContainer.removeCursorByIndex( cursorIndex );
				}
				else {
					// drag event; just update the cursor with the new position
					cursorIndex = cursorContainer.updateCursorById( id, x, y );
				}
			}
			else if ( cursor.type == MyCursor.TYPE_DIRECT_MANIPULATION ) {
				if ( type == MultitouchFramework.TOUCH_EVENT_UP ) {
					// up event
					cursorContainer.removeCursorByIndex( cursorIndex );
					this.isTranslating = false;
					this.isTransforming = false;
				}
				else {
					// drag event
					cursorIndex = cursorContainer.updateCursorById( id, x, y );

					if ( cursorContainer.getNumCursorsOfGivenType( MyCursor.TYPE_DIRECT_MANIPULATION ) == 2 ) {
						MyCursor cursor0 = cursorContainer.getCursorByType( MyCursor.TYPE_DIRECT_MANIPULATION, 0 );
						MyCursor cursor1 = cursorContainer.getCursorByType( MyCursor.TYPE_DIRECT_MANIPULATION, 1 );

						// convert cursor positions to world space
						Point2D cursor0_currentPosition_worldSpace = gw.convertPixelsToWorldSpaceUnits( cursor0.getCurrentPosition() );
						Point2D cursor1_currentPosition_worldSpace = gw.convertPixelsToWorldSpaceUnits( cursor1.getCurrentPosition() );
						Point2D cursor0_previousPosition_worldSpace = gw.convertPixelsToWorldSpaceUnits( cursor0.getPreviousPosition() );
						Point2D cursor1_previousPosition_worldSpace = gw.convertPixelsToWorldSpaceUnits( cursor1.getPreviousPosition() );
						
						//TODO update the boolean according to the last move
						this.memento.doCommand(new TransformStrokesCommand(
								this.drawing,
								selectedStrokes,
								id,
								cursor0,
								cursor1,
								cursor0_currentPosition_worldSpace,
								cursor0_previousPosition_worldSpace,
								cursor1_currentPosition_worldSpace,
								cursor1_previousPosition_worldSpace
						), this.isTransforming);
						
						this.isTransforming = true;
					}
					else if ( cursorContainer.getNumCursorsOfGivenType( MyCursor.TYPE_DIRECT_MANIPULATION ) == 1 ) {
						// convert cursor positions to world space
						Point2D cursor_currentPosition_worldSpace = gw.convertPixelsToWorldSpaceUnits( cursor.getCurrentPosition() );
						Point2D cursor_previousPosition_worldSpace = gw.convertPixelsToWorldSpaceUnits( cursor.getPreviousPosition() );
						
						//TODO update the boolean according to the last move
						this.memento.doCommand(new TranslateStrokesCommand(
								this.drawing,
								selectedStrokes,
								cursor_currentPosition_worldSpace,
								cursor_previousPosition_worldSpace
						), this.isTranslating);
						
						this.isTranslating = true;
					}
				}
			}
		}

		return true; // request a redraw

	}

	public ArrayList<Stroke> getSelectedStrokes() {
		return selectedStrokes;
	}
	
	public void setSelectedStrokes(ArrayList<Stroke> selectedStrokes) {
		this.selectedStrokes = selectedStrokes;
	}
}
